/*
 * Indexer.java
 *
 * Created on 2 September 2006, 1:52
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */
package fasea.index;

import fasea.constants.Constants;
import fasea.index.filehandler.LuceneDocumentFactory;
import fasea.model.IndexingFolder;
import fasea.model.IndexingFolderList;
import fasea.utils.FileUtil;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.SimpleFSDirectory;

/**
 *
 * @author gvasil
 * @author lars-wolfram
 */
public class Indexer {

    private Directory indexDirectory;
    private static final int MAX_FIELD_LENGTH = 102400; //100KB
    /**
     * Indexing process will stop asap when its value is true
     */
    private volatile boolean abort;
    /**
     * Indexing process has been successfully stopped
     */
    private volatile boolean stopped;
    /*
     * Statistics
     */
    private long totalBytes;
    private long totalFiles;
    private IndexerStatusListener listener;
    private IndexingFolderList indexingFolders;
    private IndexWriter indexWriter;
    private long delayInMilliseconds = 0;
    //private IndexProperties indexProperties;

    /**
     * Creates a new instance of Indexer
     */
    public Indexer(File indexFolder, IndexingFolderList indexingFolders) throws IOException {
        this.indexDirectory = new SimpleFSDirectory(indexFolder);

        this.totalBytes = 0;
        this.totalFiles = 0;

        this.stopped = true;

        this.indexingFolders = indexingFolders;

        //this.indexProperties = indexProperties;

        if (IndexReader.indexExists(indexDirectory)) {
            if (IndexWriter.isLocked(this.indexDirectory)) {
                throw new IOException("Index is locked.");
            }
        }

        IndexWriterConfig config = new IndexWriterConfig(Constants.getLuceneVersion(), Constants.getAnalyzer());
        this.indexWriter = new IndexWriter(this.indexDirectory, config);
    }

    public static boolean tryUnlockIndex() {
        try {
            Directory indexDirectory = new SimpleFSDirectory(new File(Constants.getDefaultIndexDirectoryPath()));
            if (IndexReader.indexExists(indexDirectory)) {
                if (IndexWriter.isLocked(indexDirectory)) {
                    IndexWriter.unlock(indexDirectory);
                }
            }
            return true;
        } catch (IOException e) {

            return false;
        }
    }

    @Override
    protected void finalize() {
        try {
            if(this.indexWriter != null){
                this.indexWriter.close();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        this.indexWriter = null;
    }

    public synchronized void setStatusListener(IndexerStatusListener listener) {
        this.listener = listener;
    }

    public static boolean indexExists(File directory) throws IOException {
        return IndexReader.indexExists(new SimpleFSDirectory(directory));
    }

    /**
     * Removes from this index all documents that have been deleted or modified.
     */
    private synchronized void removeModifiedDocuments() throws IOException {
        IndexReader indexReader = null;
        try {
            indexReader = IndexReader.open(this.indexDirectory);
        } catch (IOException e) {
            // index has not been created yet. just return.
            return;
        }

        if (this.listener != null) {
            this.listener.write("Removing modified documents...");
        }

        int size = indexReader.maxDoc();

        for (int i = 0; i < size && !this.abort; ++i) {

            if (indexReader.isDeleted(i)) {
                continue;
            }

            Document doc = indexReader.document(i);
            if (shouldFileDeleteFromIndex(doc)) {
                String path = doc.get("path");
                Term t = new Term("path", path);
                this.indexWriter.deleteDocuments(t);

                File file = new File(path);
                String type = FileUtil.isDirectory(file) ? "Directory" : "File";

                listener.write(type + " '" + file + "' removed. ");
                listener.setIndexDocumentsCount(indexWriter.numDocs());
            }
        }
        listener.setIndexDocumentsCount(indexWriter.numDocs());
        indexReader.close();
    }

    private boolean shouldFileDeleteFromIndex(Document doc) throws NumberFormatException {
        String path = doc.get("path");
        File file = new File(path);
        boolean shouldFileDeleteFromIndex = false;
        if (!file.exists()) {
            shouldFileDeleteFromIndex = true;
        } else {

            long l = Long.parseLong(doc.get("lastModified"));
            long s = Long.parseLong(doc.get("size"));

            if (!FileUtil.matchIndexingFolderSettings(file,indexingFolders)) {
                shouldFileDeleteFromIndex = true;
            } else if (l != file.lastModified() || s != file.length()) {
                shouldFileDeleteFromIndex = true;
            }
        }
        return shouldFileDeleteFromIndex;
    }

    private synchronized void indexFile(File file) throws IOException {
        LuceneDocumentFactory documentFactory = new LuceneDocumentFactory(false);

        String filename = file.getCanonicalPath();

        Document doc = null;

        if (this.listener != null) {
            String type;
            if (FileUtil.isDirectory(file)) {
                type = "Directory";
            } else {
                type = "File";
            }

            this.listener.write("Indexing " + type + " '" + filename + "'.");
        }

        try {
            doc = documentFactory.getDocument(file);
        } catch (Exception e) {
            if (this.listener != null) {
                this.listener.write("Error while reading " + file.getName());
            }
        }

        if (doc != null) {
            this.totalBytes += file.length();
            this.totalFiles += 1;

            this.indexWriter.addDocument(doc);

            if (this.listener != null) {
                String type;
                if (FileUtil.isDirectory(file)) {
                    type = "Directory";
                } else {
                    type = "File";
                }

                this.listener.write(type + " '" + filename + "' indexed.");
                listener.setIndexDocumentsCount(indexWriter.numDocs());
            }
        }
    }

    /**
     * Index all documents that are not contained in this index.
     */
    private void indexDocuments() throws IOException {

        indexWriter.setMaxFieldLength(Indexer.MAX_FIELD_LENGTH);

        IndexReader indexReader = IndexReader.open(indexWriter, true);


        for (IndexingFolder indexingFolder : indexingFolders) {
            FileWalker files = new FileWalker(indexingFolder);

            while (files.hasNext() && !this.abort) {

                File file = files.next();

                if (this.delayInMilliseconds > 0) {
                    try {
                        Thread.sleep(delayInMilliseconds);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                if (!isCurrentlyInIndex(indexReader, file)) {
                    this.indexFile(file);
                }
            }
        }
        listener.setIndexDocumentsCount(indexWriter.numDocs());
        indexReader.close();
    }
    
    private boolean isCurrentlyInIndex(IndexReader indexReader, File file) throws IOException {
        return indexReader.termDocs(new Term("path", file.getCanonicalPath())).next();
    }

    public synchronized void optimize() throws IOException {
        if (this.listener != null) {
            this.listener.write("Optimizing index...");
        }
        try {
            this.indexWriter.optimize();
        } catch (org.apache.lucene.store.AlreadyClosedException ex) {
            //is ok;
        }
        if (this.listener != null) {
            this.listener.write("Optimize finished.");
        }
    }

    public void startIndexing() {
        try {
            this.stopped = false;
            if (this.listener != null) {
                listener.indexerStarted();
                this.listener.write("Starting indexing...");
            }

            long start = new Date().getTime();

            if (!this.abort) {
                this.removeModifiedDocuments();
            }
            if (!this.abort) {
                this.indexDocuments();
            }

            long end = new Date().getTime();

            long secs = (end - start) / 1000;
            long mins = secs / 60;
            secs -= mins * 60;

            if (this.listener != null) {
                this.listener.write("Indexing finished. "
                        + this.totalFiles + " files (" + this.totalBytes
                        + " bytes) indexed in " + mins + "m:" + secs + "s");
                listener.indexerFinished();
            }

            this.stopped = true;
            this.abort = false;
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public synchronized void setDelay(long delayInMilliseconds) throws IllegalArgumentException {
        if (delayInMilliseconds < 0) {
            throw new IllegalArgumentException("Negative value");
        }

        this.delayInMilliseconds = delayInMilliseconds;
    }

    public synchronized long getDelay() {
        return this.delayInMilliseconds;
    }

    public void abort() {
        this.abort = true;
        close();
        listener.indexerAborted();
    }

    public void close() {
        while (this.stopped == false) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        if (this.indexWriter != null) {
            this.finalize();
        }
    }

    IndexWriter getIndexWriter() {
        return indexWriter;
    }
}
